﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace Scheduler.Business_Logic_Layer
{
    /**Class: GenerateAllPossibleSchedules
    * @author Victor Moy
    * @version 3.0
    * Course : ITEC 4860 Spring 2012
    * Written: March 22, 2012
    *
    *This class uses 2 recursive algorithms to generate all possible schedules and store them
    *to be later scored for being best
    * 
    *
    * Purpose: creates every possible combination of employee availability and staffing requirements for every half hour duration
    */
    class GenerateAllPossibleSchedules
    {
        //List of string that were used as primary keys for DayID in DB      
        List<string> daysOfWeekList = new List<string>(Presentation_Layer.GlobalVariables.GlobalWeekDays);

        //List of ints to store a number version of the days of the week which are better for calculations
        List<int> validDaysOfWeekAsNumbers = new List<int>();
        
        //From DB; Employees avaiability at every 30 min duration
       private Dictionary<String, List<string>> employeeHours = new Dictionary<String, List<string>>();

        //Combinations of possible staffing for each 30 min duration; 
        //[durationKeys][index of staffing possibility][staff possibilities as a list of employee names]
        private Dictionary<String, Dictionary<int,List<string>>> possibleStaffingPerDuration = new Dictionary<String, Dictionary<int,List<string>>>();
       
        //List of all possible schedules
        private List<Dictionary<String, List<string>>> schedules = new List<Dictionary<String, List<string>>>();
       
        //From the DB, durations as keys and the number of employees staffed as values
        private Dictionary<String, int> staffingRequirements = new Dictionary<String, int>();
      
        //list of relevant 30 min durations used as keys for dictionary data structures
        private List<String> durationsKey = new List<String>();
        
        //list of all possible 30 min durations used as keys for dictionary data structures
        private List<String> allDurationsKey = new List<String>();
        
        //object to scores schedules after be generated
        private ScoreSchedules score;

        //employee preferred times
        private Dictionary<string, List<string>> preferredTimes = new Dictionary<string, List<string>>();

        //allow the user to specify the date for the schedule
        private string selectedDate;

        //This is the multiplier for days of the week when representing time as a single integer representing
        //day:hour:minutes. Days of the week are the 10,000 place, hours are the 1000's and 100's place, and 
        //minutes are the 10's and 1's palce.
        public const int dayMagnitude = 10000;

        //This is the multiplier for minutes of the week when representing time as a single integer representing
        //day:hour:minutes. Days of the week are the 10,000 place, hours are the 1000's and 100's place, and 
        //minutes are the 10's and 1's palce.
        public const int hourMagnitude = 100;


        //Used for a hack to prevent index out of bounds for first time requirements are set
        //after start and end time has been changed for a day
        private List<int> numberOfDurationsPerDay = new List<int>();


        /** Method: GenerateAllPossibleSchedules
          * Constructor for this class. Instantiates all class variables
          * 
          */
        public GenerateAllPossibleSchedules(string selDate)
        {
            //set selected date
            selectedDate = selDate;
           
            //fill list of days business is open
            fillDays();
           
            //fill only relevent 30 min durations that bussiness is open for DB
            fillDurations();

            //fill all possible 30 min durations
            fillAllDurations();

            //fill staffing requirements
            fillStaffingRequirements();

            //fill employee availibility from DB
            fillEmpHours();

            //sets staffing requirements to equal number of employees available when not enough employees are
            //available to work a time slot
            compareStaffReqToEmpHours();

            //removes zeroes from staffing requirements introduced by compareStaffReqToEmpHours()
            filterZeroesFromStaffReqs();

            //generates every possible combination of availible employees for each 30 duration
            foreach (String key in durationsKey)
            {
                List<string> temporaryPossbleShiftStaffing = new List<string>();
                int index = 0;
                generatePossibleShiftsStaffings(index,key, temporaryPossbleShiftStaffing);
            }


            //geneartes every possible schedule from combinations of availible employees for each 30 duration
            generateSchedulesShortCircuited(0, 0, new Dictionary<string, List<string>>());

            //if the shortcircuited algorithm doesn't generate a schedule, the original algorithm is used instead
            if(schedules.Count() <= 0)
                generateSchedules(0, 0, new Dictionary<string, List<string>>());

            //initiation of schedule scoring algorithm
           score = new ScoreSchedules(schedules, allDurationsKey,daysOfWeekList,preferredTimes,selectedDate);   
           
        }


        /** Method: fillDays
          * fills days of the week
         * */
        private void fillDays()
        {
            //temporary set of numbered days uses every day of the week
            for( int dayNumber = 0;dayNumber<7;dayNumber++)
            validDaysOfWeekAsNumbers.Add(dayNumber);
        }

        /** Method: fillDurations
          * fills relevant 30 min durations as keys to be used in other data structures
         * */
        private void fillDurations()
        {
            List<int> staffingNumbers = new List<int>();
            foreach (int dayNumber in validDaysOfWeekAsNumbers)
            {
                int days = dayNumber * dayMagnitude;
                //Accounts for days that start at "hour:30"
                int offset = 0;
                //keeps count of how many durations we expect in a day
                int durationsCounter = 0;
                //this is an string to tack to the front of Sunday durations
                string zeroFroSunday = "";

                if (dayNumber == 0)
                    zeroFroSunday = "0";

                if (Presentation_Layer.GlobalVariables.GlobalOperationWeek[dayNumber].isOpen())
                {
                    //checks if the start time for a day starts at hour:30
                    if (Presentation_Layer.GlobalVariables.GlobalOperationWeek[dayNumber].getStart().Substring(3, 2).Equals("30"))
                    {
                        durationsKey.Add(zeroFroSunday + (days + Convert.ToInt32(Presentation_Layer.GlobalVariables.GlobalOperationWeek[dayNumber].getStart().Substring(0, 2)) * hourMagnitude + 30));
                        offset = 1;
                        durationsCounter++;
                    }

                    for (int hours = (Convert.ToInt32(Presentation_Layer.GlobalVariables.GlobalOperationWeek[dayNumber].getStart().Substring(0, 2)) + offset) * hourMagnitude; hours <= Convert.ToInt32(Presentation_Layer.GlobalVariables.GlobalOperationWeek[dayNumber].getEnd().Substring(0, 2)) * hourMagnitude; hours = hours + 100)
                    {
                        //2 duartions per hour for 30 min durations
                        durationsKey.Add(zeroFroSunday + (days + hours));
                        durationsKey.Add(zeroFroSunday + (days + hours + 30));
                        durationsCounter = durationsCounter + 2;
                    }

                    //removes extra duration if the endtime doesn't end at hour:30
                    if (Presentation_Layer.GlobalVariables.GlobalOperationWeek[dayNumber].getEnd().Substring(3, 2).Equals("30") == false)
                    {
                        durationsKey.RemoveAt(durationsKey.Count - 1);
                        durationsCounter--;
                    }
                    //gets rid of time at end time
                    durationsKey.RemoveAt(durationsKey.Count - 1);
                    durationsCounter--;
                }
                //counts how many time slots there are per day
                numberOfDurationsPerDay.Add(durationsCounter++);

            }
            foreach (string durationKey in durationsKey)
            {
                possibleStaffingPerDuration.Add(durationKey, new Dictionary<int, List<string>>());
            }
        }

        /// <summary>
        /// Method: fillStaffingRequirements
        ///Gets all relevant staffing requirements according to operating hours
        /// </summary>
        /// 
        private void fillStaffingRequirements()
        {
            //keeps count of which index in durationsKey the iterations are on
            int durationCounter = 0;
            foreach (int dayNumber in validDaysOfWeekAsNumbers)
            {
                
                if (Presentation_Layer.GlobalVariables.GlobalOperationWeek[dayNumber].isOpen())
                {

                    //This iterates the exact number of slots we are expecting to have
                    //Addresses bug that adds an extra time with value 0 when staffing requirements are changed
                    //in GUI and Schedule generation is immediate started
                    for (int durationEnumeration = 0; durationEnumeration < numberOfDurationsPerDay.ElementAt(dayNumber); durationEnumeration++)
                        {
                            //Excludes zeroes from staffing requirements from the raw data from DB. 
                            //prevents a 0 requirement from running through schedule Geneartion Alogorithm causing early termination
                            // of the recursive algorithm and no schedules to be reproduced
                            if (Presentation_Layer.GlobalVariables.GlobalOperationWeek[dayNumber].getNumPerTimeSlot().ElementAt(durationEnumeration) != 0)
                            {
                                staffingRequirements.Add(durationsKey[durationCounter], Presentation_Layer.GlobalVariables.GlobalOperationWeek[dayNumber].getNumPerTimeSlot().ElementAt(durationEnumeration));
                                durationCounter++;
                            }
                            //Removes a duration from the keys at a time slot that has a zero for staff required.
                            else
                                durationsKey.RemoveAt(durationCounter);
                        }
                    
                }
            }
            
        }

       /// <summary>
       /// Method: fillAllDurations
       /// fills relevant 30 min durations as keys to be used in other data structures
       /// </summary>
       /// 
        private void fillAllDurations()
        {
            //last day of the week is Saturday with index 6
            for (int days = 0; days <= dayMagnitude*6; days = days + dayMagnitude)
            {
                //this is an string to tack to the front of Sunday durations
                string sundayZeros = "";

                if (days == 0)
                    sundayZeros = "0";

                for (int hours = 0; hours <= 2300; hours = hours + hourMagnitude)
                {
                    allDurationsKey.Add(sundayZeros  + (days + hours));
                    allDurationsKey.Add(sundayZeros  + (days + hours + 30));
                }
                
            }

            //Creates list of all possible 30 min durations
            foreach (string allKeys in allDurationsKey)
            {
                employeeHours.Add(allKeys,new List<string>());
               
            }
        }


/// <summary>
/// Method: fillEmpHours
/// fills employee availability according to DB
/// </summary>
/// 
        private void fillEmpHours()
        {
            //Fills global variables for employee list if they haven't been
            if (!Presentation_Layer.GlobalVariables.GlobalEmployeeListCompiled)
            {
                Business_Logic_Layer.GlobalDataManager globalDataMgr = new Business_Logic_Layer.GlobalDataManager();
                globalDataMgr.CreateEmployeeList();
            }
            
            foreach (Employee employee in Presentation_Layer.GlobalVariables.GlobalEmployeeList)
            {

                //Raw DB data from a row
                List<List<object>> dbData = new List<List<object>>();
                preferredTimes.Add(employee.EmpID.Trim(), new List<string>());
                foreach (String dayID in daysOfWeekList)
                {
                    Data_Access_Layer.EmpDayInfo empDayInfo = new Data_Access_Layer.EmpDayInfo(employee.EmpID.Trim(),dayID);
                    bool test = empDayInfo.locateEmpDay();
                    List<object> dbDataForDay = new List<object>();

                    Data_Access_Layer.AcctManagement activeChecker = new Data_Access_Layer.AcctManagement(employee.EmpID.Trim());
                    activeChecker.locateUser();
                    if (activeChecker.getIsAcctActive())
                    {
                        //array has 50 objects first 2 are EmpID and DayID indexes 2-49 are statuses
                        foreach (object o in empDayInfo.getAllAvailHr())
                        {
                            dbDataForDay.Add(o);
                        }
                        dbData.Add(dbDataForDay);
                       
                    }
                   
                }

                //Counts through how many durations the loop has gone through
                int indexCounter = 0;

                //Judges if employee is avaiable or preferred for a 30min duration
                foreach (List<object> data in dbData)
                {
                    for (int dBIndex = 2; dBIndex < 50; dBIndex++)
                    {
                        if (data[dBIndex] != null)
                        {
                            //For the algorithms to use
                            if (Convert.ToString(data[dBIndex]).Trim().Equals("A") || Convert.ToString(data[dBIndex]).Trim().Equals("P"))
                            {
                                employeeHours[allDurationsKey[indexCounter]].Add(employee.EmpID.Trim());
                            }

                            //used in scoring prefferred hours
                            if (Convert.ToString(data[dBIndex]).Trim().Equals("P"))
                            {
                                preferredTimes[employee.EmpID.Trim()].Add(allDurationsKey[indexCounter]);
                            }
                        }

                        indexCounter++;
                    }
                }
            }
        }

        /// <summary>
        /// This method changes a staffing requirement to equal the number of employees available for a 30 minute
        /// duration if the staffing requirement is higher than the number of employees available at the slot.
        /// </summary>
        private void compareStaffReqToEmpHours()
        {
            int durationCounter = 1;
            foreach (string durationKey in durationsKey)
            {
                if (employeeHours[durationKey].Count() < staffingRequirements[durationKey])
                {
                    MessageBox.Show("Requirement at slot " + durationCounter + " on " + Convert.ToDateTime(selectedDate).StartOfWeek(DayOfWeek.Sunday).Add(new TimeSpan(Convert.ToInt32(durationKey.ElementAt(0)), 0, 0, 0)) + " could not be satisfied.");
                    staffingRequirements[durationKey] = employeeHours[durationKey].Count();
                }
                durationCounter++;
            }
        }


        /// <summary>
        /// This method is meant to filter out staffing requiremnts with a value of zero 
        /// introduced by there being no employees available to work a particular shift
        /// </summary>
        private void filterZeroesFromStaffReqs()
        {
            int durationCounter = 0;
            List<int> removalList = new List<int>();
            foreach (string durationKey in durationsKey)
            {
                if (staffingRequirements[durationKey] == 0)
                {
                    staffingRequirements.Remove(durationKey);
                    removalList.Add(durationCounter);
                }
                durationCounter++;
            }
            
                for(int removalIndex = removalList.Count()-1;removalIndex>0;removalIndex--)
                {
                    durationsKey.RemoveAt(removalIndex);  
                }
            
        }

/// <summary>
/// Method: generatePossibleShiftsStaffings
///generates every possible combination of availible employees for each 30 duration
/// </summary>
/// <param name="index">A counter to stop recursion</param>
/// <param name="key">key 30 min duration key</param>
/// <param name="temporaryPossbleShiftStaffing">temporaryPossbleShiftStaffing is temporary storage for employee names</param>
/// <returns>List that is arbitrary</returns>
/// 
        private List<string> generatePossibleShiftsStaffings(int index,string key, List<string> temporaryPossbleShiftStaffing)
        {
            if (index >= employeeHours[key].Count)
            {
                return temporaryPossbleShiftStaffing;
            }
            else
            {
                temporaryPossbleShiftStaffing.Add(employeeHours[key].ElementAt(index));
            }

            if (temporaryPossbleShiftStaffing.Count() == staffingRequirements[key])
                {
                   
                   
                    List<string> copyList= new List<string>(temporaryPossbleShiftStaffing);
                    possibleStaffingPerDuration[key].Add(possibleStaffingPerDuration[key].Count-1, copyList);
                   
                }
                
                index++;
                generatePossibleShiftsStaffings(index,key, temporaryPossbleShiftStaffing);
                temporaryPossbleShiftStaffing.RemoveAt(temporaryPossbleShiftStaffing.Count - 1);
                generatePossibleShiftsStaffings(index, key, temporaryPossbleShiftStaffing);
         
            return temporaryPossbleShiftStaffing;
        }


       
       /// <summary>
       /// Method: generateSchedules
       ///generates every schedule
       /// </summary>
       /// <param name="keyIndex">A counter to stop recursion</param>
       /// <param name="index">Advances through combinations of employee names availiable for a duration</param>
       /// <param name="tempSchedule">temporary storage for a schedule</param>
       /// <returns>Is arbitrary.</returns>
       /// 
       private Dictionary<string,List<string>> generateSchedules(int keyIndex,int index,Dictionary<string,List<string>> tempSchedule)
       {
           if (keyIndex >= durationsKey.Count)
           {
               Dictionary<string, List<string>> tempScheduleCopy = new Dictionary<string, List<string>>(tempSchedule);
               schedules.Add(tempScheduleCopy);
               return tempSchedule;
           }
           if (index >= possibleStaffingPerDuration[durationsKey[keyIndex]].Count)
           {
               return tempSchedule;
           }
           
           List<string> possibleStaffingPerDurationCopy = new List<string>(possibleStaffingPerDuration[durationsKey[keyIndex]][index-1]);
           tempSchedule.Add(durationsKey[keyIndex], possibleStaffingPerDurationCopy);
           keyIndex++;
           generateSchedules(keyIndex, index, tempSchedule);
           index++;
           tempSchedule.Remove(durationsKey[keyIndex-1]);
           generateSchedules(keyIndex-1, index, tempSchedule);
           return tempSchedule;
       }

       /// <summary>
       /// Method: generateSchedulesShortCircuited
       ///generates every schedule with the critieria that any possible staffing for a shift must have at least 1
       ///person from the previous shift in common. This is a common work place praactice that makes for better 
       ///schedules. There's a risk of not generating any schedules though, which is when the original algorithm is 
       ///used instead.
       /// </summary>
       /// <param name="keyIndex">A counter to stop recursion</param>
       /// <param name="index">Advances through combinations of employee names availiable for a duration</param>
       /// <param name="tempSchedule">temporary storage for a schedule</param>
       /// <returns>Is arbitrary.</returns>
       /// 
       private Dictionary<string, List<string>> generateSchedulesShortCircuited(int keyIndex, int index, Dictionary<string, List<string>> tempSchedule)
       {
           if (keyIndex >= durationsKey.Count)
           {
               Dictionary<string, List<string>> tempScheduleCopy = new Dictionary<string, List<string>>(tempSchedule);
               schedules.Add(tempScheduleCopy);
               return tempSchedule;
           }
           if (index >= possibleStaffingPerDuration[durationsKey[keyIndex]].Count)
           {
               return tempSchedule;
           }

           if(checkForCommonNameInPossibleStaffings(keyIndex, index, tempSchedule))
           {
                List<string> possibleStaffingPerDurationCopy = new List<string>(possibleStaffingPerDuration[durationsKey[keyIndex]][index - 1]);
                tempSchedule.Add(durationsKey[keyIndex], possibleStaffingPerDurationCopy);
                keyIndex++;
                generateSchedulesShortCircuited(keyIndex, index, tempSchedule);
                index++;
                tempSchedule.Remove(durationsKey[keyIndex - 1]);
                generateSchedulesShortCircuited(keyIndex - 1, index, tempSchedule);
           }
           else
           {
               index++;
               generateSchedulesShortCircuited(keyIndex, index, tempSchedule);
           }

           return tempSchedule;
       }


        /// <summary>
        /// Method:checkForCommonNameInPossibleStaffings
        /// Checks to see if any people from a possible staff for a duration have any employees
        /// in common with the previous duration's staffing
        /// </summary>
        /// <param name="keyIndex">index for the durationsKey</param>
        /// <param name="index">Index of possibleStaffingPerDuration</param>
        /// <param name="tempSchedule">The temporary schedule that been genearted so far from generateSchedulesShortCircuited</param>
        /// <returns></returns>
        /// 
       private bool checkForCommonNameInPossibleStaffings(int keyIndex,int index,Dictionary<string, List<string>> tempSchedule)
       {
           if (keyIndex == 0)
               return true;

           if (!durationsKey[keyIndex].ElementAt(0).Equals(durationsKey[keyIndex - 1].ElementAt(0)))
               return true;

           foreach(string currentDuration in possibleStaffingPerDuration[durationsKey[keyIndex]][index - 1])
           {
               if (tempSchedule[durationsKey[keyIndex-1]].Contains(currentDuration))
               {
                   return true;
               }
           }
           
           return false;
       }

    }
}

